fix(table): escape LIKE wildcards in $contains filter values#3949
Conversation
…ership workflow edits via sockets, ui improvements
The $contains filter operator builds an ILIKE pattern but does not
escape LIKE wildcard characters (%, _) in user-provided values.
This causes incorrect, over-broad query results when the search value
contains these characters. For example, filtering with
{ name: { $contains: "100%" } } matches any row where name
contains "100" followed by anything, not just the literal "100%".
Escape %, _, and \ in the value before interpolating into the ILIKE
pattern so that they match literally.
PR SummaryLow Risk Overview Adds a small Reviewed by Cursor Bugbot for commit b1790f3. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
Pull request overview
Fixes $contains filtering in the table SQL query builder so user-provided values containing LIKE wildcards are treated literally (avoiding overly broad matches).
Changes:
- Added
escapeLikePattern()to escape%,_, and\before building anILIKEpattern. - Updated
buildContainsClause()to use the escaped value when constructing the%...%match pattern.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** Builds case-insensitive pattern match: `data->>'field' ILIKE '%value%'` */ | ||
| function buildContainsClause(tableName: string, field: string, value: string): SQL { | ||
| const escapedField = field.replace(/'/g, "''") | ||
| return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${value}%`}` | ||
| return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${escapeLikePattern(value)}%`}` | ||
| } |
There was a problem hiding this comment.
escapeLikePattern prefixes %, _, and \ with backslashes, but the generated ILIKE expression does not specify an ESCAPE clause. Adding ESCAPE '\\' makes the semantics explicit and matches the established pattern used elsewhere (e.g. apps/sim/lib/knowledge/documents/service.ts:895-907).
| /** Escapes LIKE/ILIKE wildcard characters so they match literally */ | ||
| function escapeLikePattern(value: string): string { | ||
| return value.replace(/[\\%_]/g, '\\$&') | ||
| } | ||
|
|
||
| /** Builds case-insensitive pattern match: `data->>'field' ILIKE '%value%'` */ | ||
| function buildContainsClause(tableName: string, field: string, value: string): SQL { | ||
| const escapedField = field.replace(/'/g, "''") | ||
| return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${value}%`}` | ||
| return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${escapeLikePattern(value)}%`}` | ||
| } |
There was a problem hiding this comment.
This change fixes wildcard handling for $contains, but there are no assertions covering the new escaping behavior. Please add unit tests (likely in apps/sim/lib/table/__tests__/sql.test.ts) that verify the generated pattern/params for inputs containing %, _, and \\ (e.g. 100%, a_b, c\\d) so regressions are caught.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b1790f3. Configure here.
| /** Escapes LIKE/ILIKE wildcard characters so they match literally */ | ||
| function escapeLikePattern(value: string): string { | ||
| return value.replace(/[\\%_]/g, '\\$&') | ||
| } |
There was a problem hiding this comment.
Duplicated escapeLikePattern utility across two modules
Low Severity
The new escapeLikePattern function in sql.ts is a semantic duplicate of the existing escapeLikePattern in apps/sim/lib/knowledge/documents/service.ts. Both escape the same three characters (\, %, _) for LIKE patterns, just with slightly different implementations (single regex vs. three chained .replace calls). Having two copies risks divergent bug fixes if one is updated without the other. This could be extracted to a shared utility.
Reviewed by Cursor Bugbot for commit b1790f3. Configure here.
Greptile SummaryThis PR correctly fixes a LIKE-wildcard injection bug in the
Confidence Score: 5/5Safe to merge — the escaping logic is correct and the only finding is a missing unit test (P2). The core fix is correct: the regex and replacement produce valid PostgreSQL LIKE escape sequences in a single pass, and Drizzle's parameterised query mechanism ensures no SQL injection is possible through the value. The only gap is that no unit tests were added for the new helper or for wildcard-containing inputs, which is a P2 quality concern that does not block merge. Only Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["buildFilterClause(filter, tableName)"] --> B{operator?}
B -- "\$contains" --> C["buildContainsClause(tableName, field, value)"]
C --> D["escapeLikePattern(value)"]
D --> E["regex replace /[\\\\%_]/g with \\\\$&"]
E --> F["e.g. '100%' → '100\\%', 'a_b' → 'a\\_b'", '"a\\\\b" → "a\\\\\\\\b"']
F --> G["sql\`col ILIKE ${'%' + escaped + '%'}\`"]
G --> H["Drizzle emits parameterized query\ncol ILIKE $1"]
H --> I["PostgreSQL interprets \\% as literal %\n(default LIKE escape char = \\\\)"]
I --> J["Only exact literal matches returned"]
Reviews (1): Last reviewed commit: "fix(table): escape LIKE wildcards in $co..." | Re-trigger Greptile |
| /** Escapes LIKE/ILIKE wildcard characters so they match literally */ | ||
| function escapeLikePattern(value: string): string { | ||
| return value.replace(/[\\%_]/g, '\\$&') | ||
| } | ||
|
|
||
| /** Builds case-insensitive pattern match: `data->>'field' ILIKE '%value%'` */ | ||
| function buildContainsClause(tableName: string, field: string, value: string): SQL { | ||
| const escapedField = field.replace(/'/g, "''") | ||
| return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${value}%`}` | ||
| return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${escapeLikePattern(value)}%`}` |
There was a problem hiding this comment.
Missing tests for
escapeLikePattern and wildcard inputs
The escapeLikePattern function is a new security-relevant helper, but no tests were added for it. The existing $contains test in __tests__/sql.test.ts (line 95) only asserts the result is defined — it does not verify that wildcards are actually escaped in the generated SQL pattern.
The PR description lists three concrete scenarios to verify ($contains: "100%", $contains: "a_b", $contains: "a\\b"), but none were implemented as test cases. Consider adding unit tests directly on escapeLikePattern covering these inputs:
describe('escapeLikePattern', () => {
it('escapes percent signs', () => {
expect(escapeLikePattern('100%')).toBe('100\\%')
})
it('escapes underscores', () => {
expect(escapeLikePattern('a_b')).toBe('a\\_b')
})
it('escapes backslashes', () => {
expect(escapeLikePattern('a\\b')).toBe('a\\\\b')
})
it('leaves plain strings unchanged', () => {
expect(escapeLikePattern('john')).toBe('john')
})
})
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 29606901 | Triggered | Generic High Entropy Secret | a54dcbe | apps/sim/providers/utils.test.ts | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.


Summary
The
$containsfilter operator in the table query builder (buildContainsClause) interpolates user-provided values directly into an ILIKE pattern without escaping LIKE wildcard characters (%,_,\).This causes the filter to return incorrect, over-broad results when the search value contains these characters:
{ name: { $contains: "100%" } }matches any row wherenamecontains"100"followed by anything — not just the literal string"100%"{ name: { $contains: "user_name" } }matches"username","user name","userXname", etc. — because_matches any single character in LIKEFix
Escape
%,_, and\in the value before interpolating into the ILIKE pattern, so they are treated as literal characters. PostgreSQL uses\as the default LIKE escape character.Test plan
$containstests insql.test.tscontinue to pass (values without wildcards are unaffected)$contains: "100%"matches only rows containing the literal string"100%", not"100abc"$contains: "a_b"matches only rows containing the literal string"a_b", not"axb"